D:\a\csshw\csshw\src\cli.rs
Line | Count | Source |
1 | | //! CLI interface |
2 | | |
3 | | use crate::client::main as client_main; |
4 | | use crate::daemon::{main as daemon_main, resolve_cluster_tags}; |
5 | | use crate::utils::config::{ClientConfig, Cluster, Config, ConfigOpt, DaemonConfig}; |
6 | | use crate::utils::windows::WindowsApi; |
7 | | use crate::{ |
8 | | get_console_window_handle, init_logger, is_launched_from_gui, spawn_console_process, |
9 | | WindowsSettingsDefaultTerminalApplicationGuard, |
10 | | }; |
11 | | use clap::{ArgAction, CommandFactory, Parser, Subcommand}; |
12 | | |
13 | | #[cfg(test)] |
14 | | use mockall::{automock, predicate::*}; |
15 | | use windows::Win32::UI::HiDpi::PROCESS_PER_MONITOR_DPI_AWARE; |
16 | | |
17 | | const PKG_NAME: &str = env!("CARGO_PKG_NAME"); |
18 | | |
19 | | /// Cluster SSH tool for Windows inspired by csshX |
20 | | /// |
21 | | /// The main CLI arguments |
22 | | #[derive(Parser, Debug)] |
23 | | #[clap(author, version, about, long_about = None)] |
24 | | pub struct Args { |
25 | | /// Optional subcommand |
26 | | /// Usually not specified by the user |
27 | | #[clap(subcommand)] |
28 | | command: Option<Commands>, |
29 | | /// Optional username used to connect to the hosts |
30 | | #[clap(long, short = 'u')] |
31 | | username: Option<String>, |
32 | | /// Optional port used for all SSH connections |
33 | | #[clap(long, short = 'p')] |
34 | | port: Option<u16>, |
35 | | /// Hosts and/or cluster tag(s) to connect to |
36 | | /// |
37 | | /// Hosts or cluster tags might use brace expansion, |
38 | | /// but need to be properly quoted. |
39 | | /// |
40 | | /// E.g.: `csshw.exe "host{1..3}" hostA` |
41 | | /// |
42 | | /// Hosts can include a username which will take precedence over the |
43 | | /// username given via the `-u` option and over any ssh config value. |
44 | | /// |
45 | | /// E.g.: `csshw.exe -u user3 user1@host1 userA@hostA host3` |
46 | | /// |
47 | | /// Hosts can include a port number which will take precedence over the |
48 | | /// port given via the `-p` option. |
49 | | /// |
50 | | /// E.g.: `csshw.exe -p 33 host1:11 host2:22 host3` |
51 | | /// |
52 | | /// If no hosts are provided and the application is launched in a new console window |
53 | | /// (e.g. by double clicking the executable in the File Explorer), |
54 | | /// it will launch in interactive mode. |
55 | | #[clap(required = false, global = true)] |
56 | | hosts: Vec<String>, |
57 | | /// Enable extensive logging |
58 | | #[clap(short, long, action=ArgAction::SetTrue)] |
59 | | debug: bool, |
60 | | } |
61 | | |
62 | | /// The ``command`` CLI subcommand |
63 | | #[derive(Debug, Subcommand, PartialEq)] |
64 | | enum Commands { |
65 | | /// Subcommand that will launch a single client window |
66 | | /// |
67 | | /// connecting to the given host with the given username. |
68 | | /// It will also try to read input from a daemon via the named pipe. |
69 | | Client { |
70 | | /// Host to connect to |
71 | | host: String, |
72 | | }, |
73 | | /// Subcommand that will launch the daemon window. |
74 | | /// |
75 | | /// The daemon is responsible to launch the client windows, |
76 | | /// one for each given host. |
77 | | /// For each client a named pipe will be created and any keystrokes |
78 | | /// the daemon window receives are forwarded via the pipes to all the clients. |
79 | | /// Also handles control mode. |
80 | | Daemon {}, |
81 | | } |
82 | | |
83 | | /// Main Entrypoint struct |
84 | | /// |
85 | | /// Used to implement the entrypoint functions of the different |
86 | | /// subcommands |
87 | | pub struct MainEntrypoint; |
88 | | |
89 | | /// Trait for Args operations to enable mocking in tests |
90 | | #[cfg_attr(test, automock)] |
91 | | pub trait ArgsCommand { |
92 | | /// Print help message |
93 | | fn print_help(&self) -> Result<(), std::io::Error>; |
94 | | } |
95 | | |
96 | | /// Default implementation of ArgsCommand trait |
97 | | pub struct CLIArgsCommand; |
98 | | |
99 | | impl ArgsCommand for CLIArgsCommand { |
100 | 0 | fn print_help(&self) -> Result<(), std::io::Error> { |
101 | 0 | return Args::command().print_help(); |
102 | 0 | } |
103 | | } |
104 | | |
105 | | /// Trait for logger initialization to enable mocking in tests |
106 | | #[cfg_attr(test, automock)] |
107 | | pub trait LoggerInitializer { |
108 | | /// Initialize logger with the given name |
109 | | fn init_logger(&self, name: &str); |
110 | | } |
111 | | |
112 | | /// Default implementation of LoggerInitializer trait |
113 | | pub struct CLILoggerInitializer; |
114 | | |
115 | | impl LoggerInitializer for CLILoggerInitializer { |
116 | 0 | fn init_logger(&self, name: &str) { |
117 | 0 | init_logger(name); |
118 | 0 | } |
119 | | } |
120 | | |
121 | | /// Trait for writing output to enable dependency injection and testing |
122 | | #[cfg_attr(test, automock)] |
123 | | pub trait Output { |
124 | | /// Write a line to the output |
125 | | fn println(&mut self, text: &str); |
126 | | /// Write text without a newline to the output |
127 | | fn print(&mut self, text: &str); |
128 | | /// Write a line to stderr |
129 | | fn eprintln(&mut self, text: &str); |
130 | | /// Flush the output |
131 | | fn flush(&mut self); |
132 | | } |
133 | | |
134 | | /// Default implementation of Output trait that writes to stdout/stderr |
135 | | pub struct CLIOutput; |
136 | | |
137 | | impl Output for CLIOutput { |
138 | 0 | fn println(&mut self, text: &str) { |
139 | 0 | println!("{text}"); |
140 | 0 | } |
141 | | |
142 | 0 | fn print(&mut self, text: &str) { |
143 | 0 | print!("{text}"); |
144 | 0 | } |
145 | | |
146 | 0 | fn eprintln(&mut self, text: &str) { |
147 | 0 | eprintln!("{text}"); |
148 | 0 | } |
149 | | |
150 | 0 | fn flush(&mut self) { |
151 | | use std::io::Write; |
152 | 0 | std::io::stdout().flush().unwrap(); |
153 | 0 | } |
154 | | } |
155 | | |
156 | | /// Trait for reading input to enable dependency injection and testing |
157 | | #[cfg_attr(test, automock)] |
158 | | pub trait Input { |
159 | | /// Read a line from stdin |
160 | | fn read_line(&mut self) -> Result<String, std::io::Error>; |
161 | | } |
162 | | |
163 | | /// Default implementation of Input trait that reads from stdin |
164 | | pub struct CLIInput; |
165 | | |
166 | | impl Input for CLIInput { |
167 | 0 | fn read_line(&mut self) -> Result<String, std::io::Error> { |
168 | 0 | let mut input = String::new(); |
169 | 0 | std::io::stdin().read_line(&mut input)?; |
170 | 0 | return Ok(input); |
171 | 0 | } |
172 | | } |
173 | | |
174 | | /// Trait for environment operations to enable dependency injection and testing |
175 | | #[cfg_attr(test, automock)] |
176 | | pub trait Environment { |
177 | | /// Get current executable path |
178 | | fn current_exe(&self) -> Result<std::path::PathBuf, std::io::Error>; |
179 | | /// Set current directory |
180 | | fn set_current_dir(&self, path: &std::path::Path) -> Result<(), std::io::Error>; |
181 | | } |
182 | | |
183 | | /// Default implementation of Environment trait |
184 | | pub struct CLIEnvironment; |
185 | | |
186 | | impl Environment for CLIEnvironment { |
187 | 0 | fn current_exe(&self) -> Result<std::path::PathBuf, std::io::Error> { |
188 | 0 | return std::env::current_exe(); |
189 | 0 | } |
190 | | |
191 | 0 | fn set_current_dir(&self, path: &std::path::Path) -> Result<(), std::io::Error> { |
192 | 0 | return std::env::set_current_dir(path); |
193 | 0 | } |
194 | | } |
195 | | |
196 | | /// Trait for configuration management to enable dependency injection and testing |
197 | | #[cfg_attr(test, automock)] |
198 | | pub trait ConfigManager { |
199 | | /// Load configuration from the specified path |
200 | | fn load_config(&self, path: &str) -> Result<ConfigOpt, confy::ConfyError>; |
201 | | /// Store configuration to the specified path |
202 | | fn store_config(&self, path: &str, config: &Config) -> Result<(), confy::ConfyError>; |
203 | | } |
204 | | |
205 | | /// Default implementation of ConfigManager trait |
206 | | pub struct CLIConfigManager; |
207 | | |
208 | | impl ConfigManager for CLIConfigManager { |
209 | 0 | fn load_config(&self, path: &str) -> Result<ConfigOpt, confy::ConfyError> { |
210 | 0 | return confy::load_path(path); |
211 | 0 | } |
212 | | |
213 | 0 | fn store_config(&self, path: &str, config: &Config) -> Result<(), confy::ConfyError> { |
214 | 0 | return confy::store_path(path, config); |
215 | 0 | } |
216 | | } |
217 | | |
218 | | /// Trait defining the entrypoint functions of the different |
219 | | /// subcommands |
220 | | #[cfg_attr(test, automock)] |
221 | | pub trait Entrypoint { |
222 | | /// Entrypoint for the client subcommand |
223 | | fn client_main<W: WindowsApi + 'static>( |
224 | | &mut self, |
225 | | windows_api: &W, |
226 | | host: String, |
227 | | username: Option<String>, |
228 | | port: Option<u16>, |
229 | | config: &ClientConfig, |
230 | | ) -> impl std::future::Future<Output = ()> + Send; |
231 | | /// Entrypoint for the daemon subcommand |
232 | | fn daemon_main<W: WindowsApi + Clone + 'static>( |
233 | | &mut self, |
234 | | windows_api: &W, |
235 | | hosts: Vec<String>, |
236 | | username: Option<String>, |
237 | | port: Option<u16>, |
238 | | config: &DaemonConfig, |
239 | | clusters: &[Cluster], |
240 | | debug: bool, |
241 | | ) -> impl std::future::Future<Output = ()> + Send; |
242 | | /// Entrypoint for the main command |
243 | | fn main<W: WindowsApi + 'static, C: ConfigManager + 'static>( |
244 | | &mut self, |
245 | | windows_api: &W, |
246 | | config_manager: &C, |
247 | | config_path: &str, |
248 | | config: &Config, |
249 | | args: Args, |
250 | | ); |
251 | | } |
252 | | |
253 | | impl Entrypoint for MainEntrypoint { |
254 | 0 | async fn client_main<W: WindowsApi>( |
255 | 0 | &mut self, |
256 | 0 | windows_api: &W, |
257 | 0 | host: String, |
258 | 0 | username: Option<String>, |
259 | 0 | port: Option<u16>, |
260 | 0 | config: &ClientConfig, |
261 | 0 | ) { |
262 | 0 | client_main(windows_api, host, username, port, config).await; |
263 | 0 | } |
264 | | |
265 | 0 | async fn daemon_main<W: WindowsApi + Clone + 'static>( |
266 | 0 | &mut self, |
267 | 0 | windows_api: &W, |
268 | 0 | hosts: Vec<String>, |
269 | 0 | username: Option<String>, |
270 | 0 | port: Option<u16>, |
271 | 0 | config: &DaemonConfig, |
272 | 0 | clusters: &[Cluster], |
273 | 0 | debug: bool, |
274 | 0 | ) { |
275 | 0 | daemon_main(windows_api, hosts, username, port, config, clusters, debug).await; |
276 | 0 | } |
277 | | |
278 | 7 | fn main<W: WindowsApi + 'static, C: ConfigManager + 'static>( |
279 | 7 | &mut self, |
280 | 7 | windows_api: &W, |
281 | 7 | config_manager: &C, |
282 | 7 | config_path: &str, |
283 | 7 | config: &Config, |
284 | 7 | args: Args, |
285 | 7 | ) { |
286 | 7 | config_manager.store_config(config_path, config).unwrap(); |
287 | | |
288 | 7 | let mut daemon_args: Vec<String> = Vec::new(); |
289 | 7 | if args.debug { |
290 | 2 | daemon_args.push("-d".to_string()); |
291 | 5 | } |
292 | 7 | if let Some(username3 ) = args.username { |
293 | 3 | daemon_args.push("-u".to_string()); |
294 | 3 | daemon_args.push(username); |
295 | 4 | } |
296 | 7 | if let Some(port3 ) = args.port { |
297 | 3 | daemon_args.push("-p".to_string()); |
298 | 3 | daemon_args.push(port.to_string()); |
299 | 4 | } |
300 | 7 | daemon_args.push("daemon".to_string()); |
301 | | // Order is important here. If the hosts are passed before the daemon subcommand |
302 | | // it will not be recognizes as such and just be passed along as one of the hosts. |
303 | 7 | daemon_args.extend( |
304 | 7 | resolve_cluster_tags( |
305 | 9 | args.hosts.iter()7 .map7 (|host| return &**host).collect7 (), |
306 | 7 | &config.clusters, |
307 | | ) |
308 | 7 | .into_iter() |
309 | 9 | .map7 (|host| return host.to_string()), |
310 | | ); |
311 | 7 | let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new(); |
312 | | // We must wait for the window to actually launch before dropping the _guard as we might otherwise |
313 | | // reset the configuration before the window was launched |
314 | 7 | let _ = get_console_window_handle( |
315 | 7 | windows_api, |
316 | 7 | spawn_console_process(windows_api, &format!("{PKG_NAME}.exe"), daemon_args) |
317 | 7 | .expect("Failed to create process") |
318 | 7 | .dwProcessId, |
319 | 7 | ); |
320 | 7 | } |
321 | | } |
322 | | |
323 | | /// Display the interactive mode prompt and instructions |
324 | 9 | fn show_interactive_prompt<O: Output>(output: &mut O) { |
325 | 9 | output.println("\n=== Interactive Mode ==="); |
326 | 9 | output.println(&format!( |
327 | 9 | "Enter your {PKG_NAME} arguments (or press Enter to exit):" |
328 | 9 | )); |
329 | 9 | output.println("Example: -u myuser host1 host2 host3"); |
330 | 9 | output.println("Example: --help"); |
331 | 9 | output.print("> "); |
332 | 9 | output.flush(); |
333 | 9 | } |
334 | | |
335 | | /// Read user input from stdin |
336 | | /// |
337 | | /// # Arguments |
338 | | /// |
339 | | /// * `input` - The Input trait object for reading from stdin |
340 | | /// |
341 | | /// # Returns |
342 | | /// |
343 | | /// * `Ok(Some(input))` - User provided input |
344 | | /// * `Ok(None)` - User wants to exit (empty input or "exit") |
345 | | /// * `Err(error)` - Error reading input |
346 | 13 | fn read_user_input<I: Input>(input: &mut I) -> Result<Option<String>, std::io::Error> { |
347 | 13 | let input_line11 = input.read_line()?2 ; |
348 | | |
349 | 11 | let input_trimmed = input_line.trim(); |
350 | 11 | if input_trimmed.is_empty() || input_trimmed.to_lowercase() == "exit"6 { |
351 | 7 | return Ok(None); |
352 | 4 | } |
353 | | |
354 | 4 | return Ok(Some(input_trimmed.to_string())); |
355 | 13 | } |
356 | | |
357 | | /// Handle special commands that don't need full parsing |
358 | | /// |
359 | | /// # Arguments |
360 | | /// |
361 | | /// * `input` - The user input string |
362 | | /// * `args_command` - The ArgsCommand trait object for printing help |
363 | | /// |
364 | | /// # Returns |
365 | | /// |
366 | | /// * `true` - Command was handled, continue loop |
367 | | /// * `false` - Command needs full parsing |
368 | 12 | fn handle_special_commands<A: ArgsCommand>(input: &str, args_command: &A) -> bool { |
369 | 12 | if input == "--help" || input == "-h"10 { |
370 | 3 | let _ = args_command.print_help(); |
371 | 3 | return true; |
372 | 9 | } |
373 | 9 | return false; |
374 | 12 | } |
375 | | |
376 | | /// Execute a parsed command using the provided entrypoint |
377 | 11 | async fn execute_parsed_command< |
378 | 11 | W: WindowsApi + Clone + 'static, |
379 | 11 | T: Entrypoint, |
380 | 11 | A: ArgsCommand, |
381 | 11 | L: LoggerInitializer, |
382 | 11 | C: ConfigManager + 'static, |
383 | 11 | >( |
384 | 11 | windows_api: &W, |
385 | 11 | parsed_args: Args, |
386 | 11 | entrypoint: &mut T, |
387 | 11 | args_command: &A, |
388 | 11 | logger_initializer: &L, |
389 | 11 | config_manager: &C, |
390 | 11 | config: &Config, |
391 | 11 | config_path: &str, |
392 | 11 | ) { |
393 | 6 | match &parsed_args.command { |
394 | 3 | Some(Commands::Client { host }) => { |
395 | 3 | if parsed_args.debug { |
396 | 1 | logger_initializer.init_logger(&format!("csshw_client_{host}")); |
397 | 2 | } |
398 | 3 | entrypoint |
399 | 3 | .client_main( |
400 | 3 | windows_api, |
401 | 3 | host.to_owned(), |
402 | 3 | parsed_args.username.to_owned(), |
403 | 3 | parsed_args.port, |
404 | 3 | &config.client, |
405 | 3 | ) |
406 | 3 | .await; |
407 | | } |
408 | | Some(Commands::Daemon {}) => { |
409 | 3 | if parsed_args.debug { |
410 | 2 | logger_initializer.init_logger("csshw_daemon"); |
411 | 2 | }1 |
412 | 3 | entrypoint |
413 | 3 | .daemon_main( |
414 | 3 | windows_api, |
415 | 3 | parsed_args.hosts, |
416 | 3 | parsed_args.username, |
417 | 3 | parsed_args.port, |
418 | 3 | &config.daemon, |
419 | 3 | &config.clusters, |
420 | 3 | parsed_args.debug, |
421 | 3 | ) |
422 | 3 | .await; |
423 | | } |
424 | | None => { |
425 | 5 | if !parsed_args.hosts.is_empty() { |
426 | 3 | entrypoint.main( |
427 | 3 | windows_api, |
428 | 3 | config_manager, |
429 | 3 | config_path, |
430 | 3 | config, |
431 | 3 | parsed_args, |
432 | 3 | ); |
433 | 3 | } else { |
434 | 2 | // Show help for empty hosts |
435 | 2 | let _ = args_command.print_help(); |
436 | 2 | } |
437 | | } |
438 | | } |
439 | 11 | } |
440 | | |
441 | | /// Run the interactive mode loop for GUI launches |
442 | 4 | async fn run_interactive_mode< |
443 | 4 | W: WindowsApi + Clone + 'static, |
444 | 4 | A: ArgsCommand, |
445 | 4 | L: LoggerInitializer, |
446 | 4 | T: Entrypoint, |
447 | 4 | O: Output, |
448 | 4 | I: Input, |
449 | 4 | C: ConfigManager + 'static, |
450 | 4 | >( |
451 | 4 | windows_api: &W, |
452 | 4 | args_command: &A, |
453 | 4 | logger_initializer: &L, |
454 | 4 | mut entrypoint: T, |
455 | 4 | config_manager: &C, |
456 | 4 | config: &Config, |
457 | 4 | config_path: &str, |
458 | 4 | output: &mut O, |
459 | 4 | input: &mut I, |
460 | 4 | ) { |
461 | | loop { |
462 | 8 | show_interactive_prompt(output); |
463 | | |
464 | 8 | match read_user_input(input) { |
465 | 3 | Ok(Some(input_str)) => { |
466 | | // Handle special commands first |
467 | 3 | if handle_special_commands(&input_str, args_command) { |
468 | 1 | continue; |
469 | 2 | } |
470 | | |
471 | | // Parse the input as command line arguments |
472 | 2 | let input_args: Vec<&str> = input_str.split_whitespace().collect(); |
473 | 2 | let mut full_args = vec![PKG_NAME]; |
474 | 2 | full_args.extend(input_args); |
475 | | |
476 | 2 | match Args::try_parse_from(full_args) { |
477 | 1 | Ok(parsed_args) => { |
478 | 1 | execute_parsed_command( |
479 | 1 | windows_api, |
480 | 1 | parsed_args, |
481 | 1 | &mut entrypoint, |
482 | 1 | args_command, |
483 | 1 | logger_initializer, |
484 | 1 | config_manager, |
485 | 1 | config, |
486 | 1 | config_path, |
487 | 1 | ) |
488 | 1 | .await; |
489 | | } |
490 | 1 | Err(err) => { |
491 | 1 | output.eprintln(&format!("\nError parsing arguments: {err}")); |
492 | 1 | } |
493 | | } |
494 | | } |
495 | | Ok(None) => { |
496 | 4 | return; |
497 | | } |
498 | 1 | Err(err) => { |
499 | 1 | output.eprintln(&format!("Error reading input: {err}")); |
500 | 1 | } |
501 | | } |
502 | | } |
503 | 4 | } |
504 | | |
505 | | /// The main entrypoint |
506 | | /// |
507 | | /// Parses the CLI arguments, |
508 | | /// loads an existing config or writes the default config to disk, and |
509 | | /// calls the respective subcommand. |
510 | | /// If no subcommand is given we launch the daemon subcommand in a new window. |
511 | 10 | pub async fn main< |
512 | 10 | W: WindowsApi + Clone + 'static, |
513 | 10 | E: Entrypoint, |
514 | 10 | O: Output, |
515 | 10 | I: Input, |
516 | 10 | Env: Environment, |
517 | 10 | A: ArgsCommand, |
518 | 10 | L: LoggerInitializer, |
519 | 10 | C: ConfigManager + 'static, |
520 | 10 | >( |
521 | 10 | windows_api: &W, |
522 | 10 | args: Args, |
523 | 10 | mut entrypoint: E, |
524 | 10 | output: &mut O, |
525 | 10 | input: &mut I, |
526 | 10 | environment: &Env, |
527 | 10 | args_command: &A, |
528 | 10 | logger_initializer: &L, |
529 | 10 | config_manager: &C, |
530 | 10 | ) { |
531 | | // CRITICAL: Check GUI launch BEFORE any output to console |
532 | 10 | let launched_from_gui = is_launched_from_gui(windows_api); |
533 | | |
534 | | // Set DPI awareness programatically. Using the manifest is the recommended way |
535 | | // but conhost.exe does not do any manifest loading. |
536 | | // https://github.com/microsoft/terminal/issues/18464#issuecomment-2623392013 |
537 | 10 | if let Err(err4 ) = windows_api.set_process_dpi_awareness(PROCESS_PER_MONITOR_DPI_AWARE) { |
538 | 4 | output.eprintln(&format!( |
539 | 4 | "Failed to set DPI awareness programatically: {err:?}" |
540 | 4 | )); |
541 | 6 | } |
542 | 10 | match environment.current_exe() { |
543 | 9 | Ok(path) => match path.parent() { |
544 | 1 | None => { |
545 | 1 | output.eprintln("Failed to get executable path parent working directory"); |
546 | 1 | } |
547 | 8 | Some(exe_dir) => { |
548 | 8 | environment |
549 | 8 | .set_current_dir(exe_dir) |
550 | 8 | .expect("Failed to change current working directory"); |
551 | 8 | } |
552 | | }, |
553 | 1 | Err(_) => { |
554 | 1 | output.eprintln("Failed to get executable directory"); |
555 | 1 | } |
556 | | } |
557 | | |
558 | 10 | let config_path = format!("{PKG_NAME}-config.toml"); |
559 | 10 | let config_on_disk: ConfigOpt = config_manager.load_config(&config_path).unwrap(); |
560 | 10 | let config: Config = config_on_disk.into(); |
561 | | |
562 | 4 | match &args.command { |
563 | 2 | Some(Commands::Client { host }) => { |
564 | 2 | if args.debug { |
565 | 1 | logger_initializer.init_logger(&format!("csshw_client_{host}")); |
566 | 1 | } |
567 | 2 | entrypoint |
568 | 2 | .client_main( |
569 | 2 | windows_api, |
570 | 2 | host.to_owned(), |
571 | 2 | args.username.to_owned(), |
572 | 2 | args.port, |
573 | 2 | &config.client, |
574 | 2 | ) |
575 | 2 | .await; |
576 | | } |
577 | | Some(Commands::Daemon {}) => { |
578 | 2 | if args.debug { |
579 | 1 | logger_initializer.init_logger("csshw_daemon"); |
580 | 1 | } |
581 | 2 | entrypoint |
582 | 2 | .daemon_main( |
583 | 2 | windows_api, |
584 | 2 | args.hosts.to_owned(), |
585 | 2 | args.username.clone(), |
586 | 2 | args.port, |
587 | 2 | &config.daemon, |
588 | 2 | &config.clusters, |
589 | 2 | args.debug, |
590 | 2 | ) |
591 | 2 | .await; |
592 | | } |
593 | | None => { |
594 | | // If no hosts provided, show help and handle GUI vs console launch |
595 | 6 | if args.hosts.is_empty() { |
596 | 5 | let _ = args_command.print_help(); |
597 | | |
598 | | // If launched from GUI, allow user to input arguments interactively |
599 | 5 | if launched_from_gui { |
600 | 1 | run_interactive_mode( |
601 | 1 | windows_api, |
602 | 1 | args_command, |
603 | 1 | logger_initializer, |
604 | 1 | entrypoint, |
605 | 1 | config_manager, |
606 | 1 | &config, |
607 | 1 | &config_path, |
608 | 1 | output, |
609 | 1 | input, |
610 | 1 | ) |
611 | 1 | .await; |
612 | 4 | } |
613 | 5 | return; |
614 | 1 | } |
615 | | |
616 | 1 | entrypoint.main(windows_api, config_manager, &config_path, &config, args); |
617 | | } |
618 | | } |
619 | 10 | } |
620 | | |
621 | | #[cfg(test)] |
622 | | #[path = "./tests/test_cli.rs"] |
623 | | mod test_cli; |